home *** CD-ROM | disk | FTP | other *** search
/ Flash MX Savvy / FlashMX Savvy.iso / pc / WIN / UltraDev / UltraDev_Trial.exe / Disk1 / data1.cab / Configuration_En / Commands / Clean Up HTML.js < prev    next >
Encoding:
JavaScript  |  2000-12-11  |  35.5 KB  |  1,097 lines

  1. //
  2. // Copyright 1999 Macromedia, Inc. All rights reserved.
  3. // ----------------------------------------------------
  4. //
  5. // Clean Up HTML.js
  6. //
  7. // This command cleans up certain categories of superfluous
  8. // HTML within the user document without effecting document
  9. // layout.  This command makes two passes over the users
  10. // document depending on the options selected; see
  11. // cleanUpDocument() for more details.
  12. //
  13. // Version 3.0
  14. // Added new array class for cleanup HTML contains function.
  15. // ----------------------------------------------------
  16.  
  17. //
  18. // Global variables -- see initialize() for more comments.
  19. // Dreamweaver doesn't currently initialize any globals
  20. // loaded in auxilliary scripts through <SCRIPT SRC=..>,
  21. // so we explicitly initialize these initialize().
  22. //
  23. var helpDoc = MM.HELP_cmdCleanUpHTML;
  24.  
  25. var cbRemoveEmptyTags;
  26. var cbRemoveRedundant;
  27. var cbCombineFonts;
  28. var cbRemoveTags;
  29. var cbRemoveComments;
  30. var cbRemoveDWComments;
  31. var cbShowLog;
  32. var tbTagsToRemove;
  33. var numEmptyRemoved;
  34. var numRedundantRemoved;
  35. var numTagsRemoved;
  36. var numCommentsRemoved;
  37. var numFontsCombined;
  38. var arrTagsToRemove = new Array(); // Array
  39. var strClassAttrib;
  40. var strStyleAttrib;
  41. var arrDWCommentTags = new Array();
  42. var emptyRemovalCandidates = new Array();
  43. var redundantTagCandidates = new Array();
  44. var combinableTagCandidates = new Array();
  45. var bPreserveEmptyHeader;
  46. var bRemovedTracing;
  47.  
  48.  
  49. //------------------ Commands API --------------------
  50.  
  51. function commandButtons()
  52. {
  53.    return new Array( BTN_OK,     "cleanUpDocument()"  // main entry point
  54.                    , BTN_Cancel, "window.close()"
  55.                    , BTN_Help,   "displayHelp()");
  56. }
  57.  
  58. function canAcceptCommand()
  59. {
  60.   return (dw.getDocumentDOM() && dw.getDocumentDOM().getParseMode() == 'html' && (dw.getFocus() == 'document' || dw.getFocus(true) == 'html' || dw.getFocus() == 'textView'));
  61. }
  62.  
  63. //
  64. // ------- Local Clean Up command functions ----------
  65. //
  66.  
  67. function arrayContains( curArr, item )
  68. {
  69.    var nElements = curArr.length;
  70.    for( var i = 0; i < nElements; i++ )
  71.       if ( curArr[i] == item )
  72.          return true;
  73.  
  74.    return false;
  75. }
  76.  
  77. function isQuote( c )
  78. {
  79.    return( c == '\"' || c == '\'' );
  80. }
  81.  
  82. function isAlpha( c )
  83. {
  84.    var isoval = c.charCodeAt(0);
  85.    return( (isoval >= "A".charCodeAt(0) && isoval <= "Z".charCodeAt(0)) ||
  86.            (isoval >= "a".charCodeAt(0) && isoval <= "z".charCodeAt(0)));
  87. }
  88.  
  89. // Match a <xxx or </xxx tag; note that xxx must be alphabetical
  90. function isTagBegin( currentchar, nextchar )
  91. {
  92.    return( currentchar == '<' && (isAlpha( nextchar ) || nextchar == '/') );
  93. }
  94.  
  95. // Note that '>' should be ignored within quotes inside tag brackets
  96. function isTagEnd( c )
  97. {
  98.    return( c == '>' );
  99. }
  100.  
  101. function isWhite( c )
  102. {
  103.    return( c == ' ' || c == '\t' || c == '\n' || c == '\r' );
  104. }
  105.  
  106. function isAllWhite( str )
  107. {
  108.    for( var i = 0; i < str.length; i++ )
  109.    {
  110.       if ( !isWhite( str.charAt( i ) ) )
  111.          return( false );
  112.    }
  113.  
  114.    return( true );
  115. }
  116.  
  117. // parseAttributes()
  118. //
  119. // Parse the attributes from within the start tag of a given
  120. // node per the rules found here: http://www.w3.org/TR/WD-html-lex/
  121. //
  122. // Return an array of arrays (unfortunately; the associative
  123. // aspect of arrays is overloaded with "instance properties",
  124. // so arrays already contain prototype methods/properties
  125. // value pairs):
  126. //
  127. //          arr[0] --> attributes array
  128. //          arr[1] --> values array
  129. //
  130. // The value for a given attribute are in the same position
  131. // at the attribute within the values array.  Singleton name
  132. // tokens have an empty ("") or undefined value.
  133. //
  134. // If bStripQuotes is true, then any "outer" quotes around an
  135. // attribute value are stripped, e.g., the value in
  136. //
  137. //    NAME="bob's name"
  138. //
  139. // is returned as: bob's name.  If bStripQuotes is false, that
  140. // value is returned as "bob's name"
  141. //
  142. // If bMakeUpper is true, all attribute/value strings are normalized
  143. // to upper case
  144. //
  145. function parseAttributes( node, bStripQuotes, bMakeUpper )
  146. {
  147.    var   tagstr         = node.outerHTML;
  148.    var   pos            = 0;
  149.    var   prevChar       = null;
  150.    var   currentChar    = null;
  151.    var   currentQuote   = null;
  152.    var   arrAttribs     = new Array();
  153.    var   arrValues      = new Array();
  154.    var   arrIdx         = 0;
  155.    var   attrib         = "";
  156.    var   value          = "";
  157.    var   bValueIsEmpty  = false;
  158.    var   bInsideQuote   = false;
  159.    var   bAccumValue    = false;
  160.    var   bAttribReady   = false;
  161.    var   bSkipToWhite   = true;  // initially true to skip "<tag "
  162.  
  163.    while( pos < node.outerHTML.length )
  164.    {
  165.       prevChar     = currentChar;
  166.       currentChar  = tagstr.charAt( pos++ );
  167.  
  168.       // Handle quote state; remember actual quote that
  169.       // flipped the state so we match ' and " right
  170.       //
  171.       if ( isQuote( currentChar ) )
  172.       {
  173.          if ( bInsideQuote )
  174.          {
  175.             if ( currentChar == currentQuote )
  176.             {
  177.                // Coming out of quoted region; turn quotes off
  178.                bInsideQuote = false;
  179.                currentQuote = null;
  180.                if ( bStripQuotes )
  181.                {
  182.                   // Careful; make sure ATTR="" works even when we're
  183.                   // stripping quotes off values
  184.                   MM_assert( bAccumValue, MSG_ParseErrEndQuote );
  185.                   bValueIsEmpty = true;
  186.                   continue;
  187.                }
  188.             }
  189.          }
  190.          else
  191.          if ( bAccumValue && value == "" ) // only turn quotes on after '=' and
  192.          {                                 // before accumulating anything; e.g.,
  193.             // Turn quotes on              // ignore the quote in ATTR=xxx"xxx
  194.             bInsideQuote = true;
  195.             currentQuote = currentChar;
  196.             if ( bStripQuotes )
  197.                continue;
  198.          }
  199.       }
  200.  
  201.       // Handle the terminating character; write any attribute/value
  202.       // we may have been accumulating and we're done.
  203.       //
  204.       if ( !bInsideQuote && isTagEnd( currentChar ) )
  205.       {
  206.          if ( attrib != "" )
  207.          {
  208.             arrAttribs[ arrIdx ]  = bMakeUpper ? attrib.toUpperCase() : attrib;
  209.             arrValues[ arrIdx++ ] = bMakeUpper ? value.toUpperCase() : value;
  210.             attrib = "";
  211.             value  = "";
  212.             bAttribReady = false;
  213.             bAccumValue  = false;
  214.          }
  215.          break;
  216.       }
  217.  
  218.       // Accumulate characters; if bAccumValue is true, we're on the
  219.       // right side of an "=", otherwise we're on the left side or accumulating
  220.       // a singleton name token.  I don't think quoted regions make sense
  221.       // on the left side either.
  222.       //
  223.       if ( !bInsideQuote && !bAccumValue )
  224.       {
  225.          // first skip to white after tag name <xxxx
  226.          if ( bSkipToWhite && !isWhite( currentChar ) )
  227.             continue;
  228.  
  229.          bSkipToWhite = false;
  230.  
  231.          // Whitespace not inside quotes; if we're accumulating
  232.          // an attribute, it's ready (the whitespace terminates it);
  233.          if ( isWhite( currentChar ) )
  234.          {
  235.             bAttribReady = attrib != "";
  236.          }
  237.          else
  238.          {
  239.             // Non-white space; if we have an equals sign, switch
  240.             // over to accumulate the value
  241.             if ( currentChar == '=' )
  242.             {
  243.                bAttribReady = attrib != "";
  244.                bAccumValue  = true;
  245.                MM_assert( bAttribReady, MSG_ParseErrUnexpectedEQU );
  246.             }
  247.             else
  248.             {
  249.                // Unquoted non-white non-value -- accumulate
  250.                // as name token.  If there's a name token ready,
  251.                // save it as a singleton first.
  252.                //
  253.                if ( bAttribReady )
  254.                {
  255.                   arrAttribs[ arrIdx++ ] = bMakeUpper ? attrib.toUpperCase() : attrib;
  256.                   attrib = "";
  257.                   bAttribReady = false;
  258.                }
  259.  
  260.                attrib += currentChar;
  261.             }
  262.          }
  263.       }
  264.       else
  265.       {
  266.          // We're accumulating a value
  267.          //
  268.          MM_assert( bAttribReady, MSG_ParseErrUnexpectedEQU );
  269.  
  270.          if ( !bInsideQuote && isWhite( currentChar ) )
  271.          {
  272.             // Swallow whitespace until we either get a value
  273.             // or we terminate
  274.  
  275.             if ( value != "" || bValueIsEmpty )
  276.             {
  277.                arrAttribs[ arrIdx ]  = bMakeUpper ? attrib.toUpperCase() : attrib;
  278.                arrValues[ arrIdx++ ] = bMakeUpper ? value.toUpperCase() : value;
  279.                attrib = "";
  280.                value  = "";
  281.                bAttribReady  = false;
  282.                bAccumValue   = false;
  283.                bValueIsEmpty = false;
  284.             }
  285.          }
  286.          else
  287.          {
  288.             // We're inside a quote, or we're not terminated -- keep
  289.             // accumulating
  290.             //
  291.             value += currentChar;
  292.          }
  293.       }
  294.    }
  295.  
  296.    // We're done; package up our arrays and return them
  297.    //
  298.    MM_assert( !bAccumValue, MSG_ParseErrValue );
  299.    return new Array( arrAttribs, arrValues );
  300. }
  301.  
  302. // findCombinableParent()
  303. //
  304. // Return a parent node with which the given node may have
  305. // its attributes combined with.  This routine trusts that
  306. // caller has verified that the combineTagName is a member
  307. // of combinableTagCandidates!  A combinable parent is
  308. // a direct parent up the tree who is the parent of no
  309. // other children (which would not want to inherit the
  310. // characteristics of the given child whose attributes
  311. // will migrate up), e.g.:
  312. //
  313. // <FONT face="arial"><FONT color="blue">text</FONT></FONT>
  314. //
  315. // and
  316. //
  317. // <FONT face="arial"><B><FONT color="blue">text</FONT></B></FONT>
  318. //
  319. // are combinable, but
  320. //
  321. // <FONT face="arial"><B>x<FONT color"blue">text</FONT></B></FONT>
  322. //
  323. // is not as the 'x' textual child should not inherit the
  324. // blue characteristic.  This routine walks the "direct"
  325. // (childNodes.length == 1) parent chain.
  326. //
  327. function findCombinableParent( node, combineTagName )
  328. {
  329.    MM_assert( arrayContains(combinableTagCandidates, combineTagName ) );
  330.  
  331.    var rtnNode = null;
  332.  
  333.    while ( (node.parentNode != null)    &&
  334.         (node.parentNode.childNodes.length == 1) )
  335.    {
  336.       if ( combineTagName == node.parentNode.tagName ) {
  337.          rtnNode = node.parentNode;
  338.          break;
  339.       }
  340.       if ( node.parentNode.innerHTML == node.outerHTML ) {// parent contains only this child tree
  341.          node = node.parentNode;
  342.       } else {
  343.          break;
  344.       }
  345.    }
  346.  
  347.    return rtnNode;
  348. }
  349.  
  350. // hasRedundantParent()
  351. //
  352. // Return true if the given node is redundant with a
  353. // controlling parent.  Redundant parent/children must
  354. // have identical attribute/value sets.
  355. //
  356. function hasRedundantParent( node )
  357. {
  358.    var rc = false;
  359.  
  360.    if ( arrayContains(redundantTagCandidates,  node.tagName ) )
  361.    {
  362.       var parent  = node.parentNode;
  363.  
  364.       // Find controlling parent
  365.       while( parent != null )
  366.       {
  367.          if ( node.tagName == parent.tagName )
  368.          {
  369.             // Compare parent and child attribute name/value pairs
  370.             var cArrs   = parseAttributes( node, true, true );
  371.             var pArrs   = parseAttributes( parent, true, true );
  372.             var cNames  = cArrs[0];
  373.             var cValues = cArrs[1];
  374.             var pNames  = pArrs[0];
  375.             var pValues = pArrs[1];
  376.  
  377.             if ( cNames.length == pNames.length && cValues.length == pValues.length )
  378.             {
  379.                cNames.sort();
  380.                pNames.sort();
  381.                cValues.sort();
  382.                pValues.sort();
  383.  
  384.                var len = cNames.length;
  385.                for( var i = 0; i < len; i++ )
  386.                {
  387.                   // note in js: undefined == undefined is true
  388.                   if ( pNames[i]  != cNames[i] || cValues[i] != pValues[i] )
  389.                      break;
  390.                }
  391.  
  392.                rc = (i == len); // if we got through everything they're the same
  393.             }
  394.  
  395.             if ( rc )  // if we're redundant, we're done
  396.                break;
  397.  
  398.             // Otherwise, if we're not actually overriding anything on this
  399.             // parent, we may still be redundant with an uber parent.  Cycle through
  400.             // the child's attributes and if none are present on parent keep going
  401.             //
  402.             var bKeepGoing = true;
  403.             for( var i = 0; i < cNames.length; i++ )
  404.             {
  405.                if ( arrayContains(pNames,  cNames[i] ) )
  406.                {
  407.                   bKeepGoing = false;
  408.                   break;
  409.                }
  410.             }
  411.  
  412.             if ( !bKeepGoing )
  413.                break;
  414.          } else if ( node.tagName == 'TABLE' || parent.tagName == 'TABLE') {
  415.             break;
  416.          }
  417.  
  418.          parent = parent.parentNode;
  419.       }
  420.    }
  421.  
  422.    return rc;
  423. }
  424.  
  425. // isAllWhiteNodeSignificant()
  426. //
  427. // Given a node whose inner html is all white, this
  428. // routine examines the node's siblings and returns
  429. // true if the whitespace is significant and false
  430. // otherwise.
  431. //
  432. function isAllWhiteNodeSignificant( node )
  433. {
  434.    var siblings   = node.parentNode.childNodes;
  435.    var nSiblings  = siblings.length;
  436.    var siblingIdx = 0;
  437.  
  438.    // If we're an only child, then we really need
  439.    // to look at uncles and aunts.
  440.    if ( (nSiblings == 1) && (node.parentNode != null) && (node.parentNode.nodeType != Node.DOCUMENT_NODE) )
  441.       return( isAllWhiteNodeSignificant( node.parentNode ) );
  442.  
  443.    // Find self as parent's child first
  444.    for( ; siblingIdx < nSiblings; siblingIdx++ )
  445.       if ( siblings.item( siblingIdx ) == node )
  446.          break;
  447.  
  448.    MM_assert( siblingIdx < nSiblings, MSG_ErrParentChild );
  449.  
  450.    // If sibling to the left has trailing whitespace,
  451.    // our current all white node isn't significant.  Note
  452.    // we can just look to our immediate left rather than go
  453.    // to zero because any empty siblings to the left will
  454.    // have already been gobbled.
  455.    //
  456.    var lSibling = siblingIdx > 0 ? siblings.item( siblingIdx - 1) : null;
  457.    if ( lSibling != null )
  458.    {
  459.       if ( lSibling.nodeType == Node.TEXT_NODE )
  460.       {
  461.          if ( (lSibling.data.length > 0) &&
  462.               isWhite( lSibling.data[ lSibling.data.length - 1 ] ) )
  463.             return false;
  464.       }
  465.       else
  466.       if ( lSibling.nodeType == Node.ELEMENT_NODE )
  467.       {
  468.          // non text left sibling
  469.          if ( (lSibling.innerHTML.length > 0) &&
  470.               isWhite( lSibling.innerHTML[ lSibling.innerHTML.length - 1 ] ) )
  471.             return false;
  472.       }
  473.       // else go on to our right to determine our significance
  474.    }
  475.  
  476.    // Now see if there's significant leading whitespace to
  477.    // the immediate right that might render our all white
  478.    // current node insignificant
  479.    //
  480.    var rSibling = null;
  481.    siblingIdx++;
  482.    while( siblingIdx < nSiblings )
  483.    {
  484.       rSibling = siblings.item( siblingIdx );
  485.  
  486.       if ( rSibling.nodeType == Node.TEXT_NODE )
  487.       {
  488.          // We have a textual sibling to the right; if
  489.          // this guy doesn't have leading whitespace,
  490.          // we're significant, otherwise we're not.
  491.          if ( rSibling.data.length > 0 )
  492.             return( !isWhite( rSibling.data[0] ) );
  493.  
  494.          // else empty text node
  495.       }
  496.       else
  497.       if ( rSibling.nodeType == Node.ELEMENT_NODE )
  498.       {
  499.          // We have a non-empty non-text node to the
  500.          // right; if this guy doesn't have leading
  501.          // whitespace we're significant, otherwise not
  502.          if ( rSibling.innerHTML.length > 0 )
  503.             return( !isWhite( rSibling.innerHTML[0] ) );
  504.  
  505.          // else empty non-text node...
  506.       }
  507.  
  508.       siblingIdx++;
  509.    }
  510.  
  511.    // If we got here there's nothing interesting to the
  512.    // right of this all white node, so it's as if we're
  513.    // an only child.  The DOCUMENT_NODE check is just for
  514.    // safety; there shouldn't be a way to get that high on
  515.    // empty markup node removal...
  516.  
  517.    if ( node.parentNode != null && node.nodeType != Node.DOCUMENT_NODE )
  518.       return( isAllWhiteNodeSignificant( node.parentNode ) );
  519.  
  520.    // otherwise nothing left -- we really are insignificant...
  521.    return false;
  522. }
  523.  
  524. // isRemovableEmptyTag()
  525. //
  526. // Return true if this tag can be safely removed from the
  527. // document, false otherwise.
  528. //
  529. function isRemovableEmptyTag( tagNode )
  530. {
  531.    // First this tag must be an empty removal candidate with no class info
  532.    //
  533.    if ( arrayContains(emptyRemovalCandidates,  tagNode.tagName ) && !hasClassAttribute( tagNode ) )
  534.    {
  535.       // Short-circuit for named anchor tags; empty named anchors
  536.       // should be left alone
  537.       if ( "A" == tagNode.tagName && (null != tagNode.getAttribute( "NAME" )) )
  538.          return false;
  539.  
  540.       // If the innerHTML length is zero, it's empty and
  541.       // can be safely removed *unless* it's a heading
  542.       // tag -- the first empty heading tag after text
  543.       // forces a carriage return.
  544.       //
  545.       if ( tagNode.innerHTML.length == 0 )
  546.       {
  547.          if ( ('H' == tagNode.tagName.charAt( 0 )) && bPreserveEmptyHeader )
  548.          {
  549.             // Preserve this empty header but gobble the next one we hit
  550.             bPreserveEmptyHeader = false;
  551.             return false;
  552.          }
  553.  
  554.          return true;
  555.       }
  556.       else
  557.       if ( isAllWhite( tagNode.innerHTML ) && !isAllWhiteNodeSignificant( tagNode ) )
  558.       {
  559.          // All empty tag candidates (generally character markup)
  560.          // spanning only whitespace can also be removed if the
  561.          // tag is not within text, or if the tag to the right of
  562.          // text that ends in whitespace or to the left of text
  563.          // that begins with whitespace....
  564.          return true;
  565.       }
  566.    }
  567.  
  568.    return false;
  569. }
  570.  
  571. // Using a tracing image in Dreamweaver attaches up to four proprietary
  572. // attributes to the body tag. We want to remove these attributes if Remove
  573. // Dreamweaver Comments is checked.
  574. //
  575. function removeTracingAttrs()
  576. {
  577.   var bodyNode = dreamweaver.getDocumentDOM('document').body;
  578.  
  579.   //look for tracing attributes - if any are found, toggle
  580.   //the global boolean to true and remove all attributes
  581.   if (cbRemoveDWComments.checked){
  582.     if (bodyNode.getAttribute("tracingsrc") ||
  583.         bodyNode.getAttribute("tracingopacity") ||
  584.         bodyNode.getAttribute("tracingx") ||
  585.         bodyNode.getAttribute("tracingy"))
  586.    {
  587.      //remove all tracing image attributes
  588.      bodyNode.removeAttribute("tracingsrc");
  589.      bodyNode.removeAttribute("tracingopacity");
  590.      bodyNode.removeAttribute("tracingx");
  591.      bodyNode.removeAttribute("tracingy");
  592.      bRemovedTracing=true;
  593.    }
  594.   }
  595. }
  596.  
  597. // hasStyleAttribute()
  598. //
  599. // Return true if the given ELEMENT tag has a STYLE set
  600. //
  601. function hasStyleAttribute( tagNode )
  602. {
  603.    return( tagNode.getAttribute( strStyleAttrib ) != null );
  604. }
  605.  
  606. // hasClassAttribute()
  607. //
  608. // Return true if the given ELEMENT tag has a CLASSID set
  609. //
  610. function hasClassAttribute( tagNode )
  611. {
  612.    return( tagNode.getAttribute( strClassAttrib ) != null );
  613. }
  614.  
  615. // loadNDWCommentOffsets()
  616. //
  617. // This callback is used by the comment removal traversal
  618. // to push offsets of non-Dreamweaver comment nodes into
  619. // the userData variable passed by the comment removal pass
  620. //
  621. function loadNDWCommentOffsets( commentNode, userData )
  622. {
  623.    // MM_note( "Processing NDW comment:" + commentNode.data );
  624.  
  625.    // Server-side include comments of the form "<!-- #include... -->"
  626.    // should always be left alone!
  627.  
  628.    // eat up any leading white in comment data
  629.    var i;
  630.    for( i = 0; i < commentNode.data.length; i++ )
  631.       if ( !isWhite( commentNode.data.charAt( i ) ) )
  632.          break;
  633.  
  634.    // if we have a #include skip it, otherwise push offsets for
  635.    // removal
  636.    //
  637.    var bSkipSSIinclude = commentNode.data.substr( i, 8 ).toLowerCase() == "#include";
  638.    var bSkipSSIecho = commentNode.data.substr( i, 5 ).toLowerCase() == "#echo";
  639.    var bSkipFWtable = commentNode.data.substr( i, 7 ).toLowerCase() == "fwtable";
  640.    var bSkipFWBeginCopy = (commentNode.data.indexOf("BEGIN COPYING THE HTML") != -1);
  641.    var bSkipFWEndCopy = (commentNode.data.indexOf("STOP COPYING THE HTML HERE") != -1);
  642.    var isComment = commentNode.data.charAt(0) != "<";
  643.  
  644.    if ( !bSkipSSIinclude && !bSkipSSIecho && isComment && !bSkipFWtable && !bSkipFWBeginCopy && !bSkipFWEndCopy )
  645.       userData.push( dreamweaver.nodeToOffsets( commentNode ) );
  646.  
  647.    return true;
  648. }
  649.  
  650. // processElement()
  651. //
  652. // Process a node of ELEMENT type within the user's document
  653. // This is a callback from traverse() used during the main
  654. // removal traversal.
  655. //
  656. function processElement( elementNode )
  657. {
  658.    // MM_note( "Processing element: " + elementNode.tagName );
  659.    // Remove specific tag(s) check
  660.    //
  661.    if ( cbRemoveTags.checked &&
  662.         arrayContains(arrTagsToRemove,  elementNode.tagName ) )
  663.    {
  664.       // MM_note( "* Removing specified tag " + elementNode.outerHTML );
  665.       if ( elementNode.outerHTML == elementNode.innerHTML )
  666.          elementNode.outerHTML = "";
  667.       else
  668.          elementNode.outerHTML = elementNode.innerHTML;
  669.  
  670.       numTagsRemoved++;
  671.    }
  672.    else
  673.    {
  674.       // Don't touch tags with style information
  675.       //
  676.       if ( !hasStyleAttribute( elementNode ) )
  677.       {
  678.          // Empty tag check
  679.          //
  680.  
  681.          if ( cbRemoveEmptyTags.checked &&
  682.          (isRemovableEmptyTag( elementNode )))
  683.          {
  684.             var parent = elementNode.parentNode;
  685.  
  686.             // MM_note( "* Removing empty tag: " + elementNode.outerHTML );
  687.             elementNode.outerHTML = "";
  688.             numEmptyRemoved++;
  689.  
  690.             // Small work around DW behavior -- paragraph tags with
  691.             // children are considered "not collapsable" even if the
  692.             // children are empty.  When we remove all empty children
  693.             // of a p tag then, DW sticks in a   to keep the
  694.             // remaining <p> from being collapsed -- this makes the <p>
  695.             // then come alive in the browser layout.  So if we've just
  696.             // zapped the last child of a p tag, rewrite the P tag without
  697.             // the   so it remains collapsed in the browser layout.
  698.             // Note that if the p tag originally had text or an  
  699.             // it would still have textual children after the empty tag
  700.             // removal and would be untouched.
  701.             //
  702.             if ( parent.tagName == "P" && !(parent.hasChildNodes()) )
  703.                parent.outerHTML = "<p>";
  704.          }
  705.          // Redundant child check
  706.          //
  707.          else
  708.          if ( cbRemoveRedundant.checked &&
  709.               hasRedundantParent( elementNode ) )
  710.          {
  711.             // MM_note( "* Removing redundant tag: " + elementNode.outerHTML );
  712.             elementNode.outerHTML = elementNode.innerHTML;
  713.             numRedundantRemoved++;
  714.          }
  715.          // Child/parent coalesce check
  716.          //
  717.          else
  718.          if ( cbCombineFonts.checked &&
  719.               arrayContains(combinableTagCandidates,  elementNode.tagName ) )
  720.          {
  721.             var parent  = findCombinableParent( elementNode, elementNode.tagName );
  722.             if ( parent != null )
  723.             {
  724.                // MM_note( "* Combining font tags: " + elementNode.outerHTML );
  725.  
  726.                // Set all child attributes on parent and remove child
  727.                //
  728.                var arrs    = parseAttributes( elementNode, true, false );
  729.                var attribs = arrs[0];
  730.                var values  = arrs[1];
  731.  
  732.                for( var i = 0; i < attribs.length; i++ )
  733.                   parent.setAttribute( attribs[i], values[i] ); // The value part
  734.                                                                 // here may be null
  735.                elementNode.outerHTML = elementNode.innerHTML;
  736.                numFontsCombined++;
  737.             }
  738.          }
  739.          // Dreamweaver comment check -- dreamweaver comments
  740.          // come back to us as element nodes rather than comment nodes
  741.          else
  742.          if ( cbRemoveDWComments.checked &&
  743.               arrayContains(arrDWCommentTags,  elementNode.tagName ) )
  744.          {
  745.             // MM_note( "Removing DW comment: " + elementNode.tagName );
  746.             dreamweaver.editLockedRegions(true);
  747.             elementNode.outerHTML = elementNode.innerHTML;
  748.             numCommentsRemoved++;
  749.          }
  750.       }
  751.    }
  752.  
  753.    return true; // continue traverse
  754. }
  755.  
  756. // emptyHeaderStateTextHandler()
  757. //
  758. // This text node callback is used by pass two to flip
  759. // the global bPreserveEmptyHeader state to true -- we
  760. // just encountered text, so the next empty header
  761. // found will force a carriage return and thus can't
  762. // be removed.  Empty headers after that however can
  763. // be removed until the next piece of text is encountered...
  764. //
  765. function emptyHeaderStateTextHandler( node )
  766. {
  767.    bPreserveEmptyHeader = true;
  768.    return true;
  769. }
  770.  
  771. // traverse()
  772. //
  773. // Do a recursive depth-first traversal of the user's
  774. // document starting from the given node.
  775. //
  776. // Callers provide up to three callback functions which
  777. // accept a node argument, one each (or the same one)
  778. // to process nodes of ELEMENT, TEXT, or COMMENT type.
  779. // At least one callback function is required.
  780. //
  781. // The handlers may stop the traversal by returning false;
  782. // returning true will continue the traversal to its
  783. // completion.
  784. //
  785. // A fourth argument, a handle to a some user variable to
  786. // be passed on to each callback, may also be provided.
  787. //
  788. function traverse( node, fElementHandler ) // optional: fTextHandler, fCommentHandler, userData )
  789. {
  790.    var fTextHandler  = traverse.arguments.length >= 3 ? traverse.arguments[2] : null;
  791.    var fCmmtHandler  = traverse.arguments.length >= 4 ? traverse.arguments[3] : null;
  792.    var userData      = traverse.arguments.length >= 5 ? traverse.arguments[4] : null;
  793.    var children      = node.childNodes;
  794.    var nChildren     = children.length;
  795.    var bContinue     = true;
  796.    var current       = null;
  797.  
  798.    for( var i = 0; bContinue && (i < nChildren); i++ )
  799.    {
  800.       current = children.item( i );
  801.  
  802.       // descend to any children first
  803.       if ( current.hasChildNodes() )
  804.          traverse( current, fElementHandler, fTextHandler, fCmmtHandler, userData );
  805.  
  806.       // process current node
  807.       switch( current.nodeType )
  808.       {
  809.          case Node.ELEMENT_NODE:
  810.             if ( userData != null )
  811.                bContinue = fElementHandler( current, userData );
  812.             else
  813.                bContinue = fElementHandler( current );
  814.             break;
  815.  
  816.          case Node.COMMENT_NODE:
  817.             if ( fCmmtHandler != null )
  818.                if ( userData != null )
  819.                   bContinue = fCmmtHandler( current, userData );
  820.                else
  821.                   bContinue = fCmmtHandler( current );
  822.             break;
  823.  
  824.          case Node.TEXT_NODE:
  825.             if ( fTextHandler != null )
  826.                if ( userData != null )
  827.                   bContinue = fTextHandler( current, userData )
  828.                else
  829.                   bContinue = fTextHandler( current )
  830.             break;
  831.  
  832.          case Node.DOCUMENT_NODE:
  833.          default:
  834.              MM_error( MSG_UnknownNodeType, current.nodeType );
  835.       }
  836.    }
  837. }
  838.  
  839. // doPassOne()
  840. //
  841. // Pass one does cleanup based on the HTML source string for
  842. // the user's document; currently that means comment and extra
  843. // whitespace removal.
  844. //
  845. // BW 8/17/98 Removed "remove extra whitespace" option for
  846. //            performance reasons
  847. //
  848. function doPassOne()
  849. {
  850.    if ( cbRemoveComments.checked )  // pass one options
  851.    {
  852.       var htmlstr = dreamweaver.getDocumentDOM( 'document' ).documentElement.outerHTML;
  853.       var htmlpos = 0;
  854.       var htmlarr = new Array(); // array to save newing of intermediate
  855.                                  // string copies of doc
  856.  
  857.       // To remove comments, traverse over the entire DOM gathering
  858.       // offsets into the HTML source of the comments to be removed,
  859.       // then remove those comments from the HTML source string.
  860.       //
  861.       var root           = dreamweaver.getDocumentDOM('document');
  862.       var commentOffsets = new Array();
  863.       var stubCallback   = new Function( "node", "userData", "return true;" );
  864.  
  865.       if ( root != null && root.hasChildNodes() )
  866.          traverse( root
  867.                  , stubCallback
  868.                  , stubCallback
  869.                  , loadNDWCommentOffsets
  870.                  , commentOffsets );
  871.  
  872.       // Now use offsets to delete sections of text from
  873.       // within the document source string.
  874.       //
  875.       if ( commentOffsets.length > 0 )
  876.       {
  877.          var lastpos = 0;
  878.          for( var i = 0; i < commentOffsets.length; i++ )
  879.          {
  880.             htmlarr[htmlpos++] = htmlstr.substring( lastpos
  881.                                                   , commentOffsets[i][0] );
  882.             lastpos = commentOffsets[i][1];
  883.             numCommentsRemoved++;
  884.          }
  885.  
  886.          htmlarr[htmlpos++] = htmlstr.substring( lastpos );
  887.       }
  888.  
  889.       if ( htmlarr.length > 0 )
  890.          dreamweaver.getDocumentDOM( 'document' ).documentElement.outerHTML = htmlarr.join("");
  891.    }
  892. }
  893.  
  894. // doPassTwo()
  895. //
  896. // Pass two does cleanup on DOM objects as appropriate over the
  897. // course of traversing the DOM heirarchy.  The actual work in this
  898. // pass is done in the processElement() callback.
  899. //
  900. function doPassTwo()
  901. {
  902.    // Load up comma-separated list of tags to remove if any; warn
  903.    // if option is checked but no tags specified
  904.    //
  905.    arrTagsToRemove = dreamweaver.getTokens( tbTagsToRemove.value.toUpperCase(), ", " );
  906.    if ( cbRemoveTags.checked && arrTagsToRemove.length == 0 )
  907.       MM_error( MSG_NoTagsToRemove );
  908.  
  909.    // Traverse document, processing leaves
  910.    //
  911.    var root = dreamweaver.getDocumentDOM('document');
  912.  
  913.    if ( root != null && root.hasChildNodes() )
  914.    {
  915.       traverse( root
  916.               , processElement
  917.               , emptyHeaderStateTextHandler ); // no comment handler for this pass
  918.  
  919.       // and finally attempt to remove tracingsrc attributes
  920.       // in body tag
  921.       //
  922.       removeTracingAttrs();
  923.    }
  924.    else
  925.       MM_error( MSG_ErrEmptyDoc );
  926.  
  927. }
  928.  
  929. // cleanUpDocument()
  930. //
  931. // Main routine for performing clean up when user hits OK.
  932. // Clean up is done in three passes:
  933. //
  934. // Pass 1: Clean up certain items based on the entire HTML
  935. //         document as a string
  936. // Pass 2: Clean up certain items while traversing the DOM
  937. //
  938. function cleanUpDocument()
  939. {
  940.    // Set up logging particulars
  941.    //
  942.    if ( cbShowLog.checked )
  943.    {
  944.       MM_enableLogging();
  945.       MM_clearLog();
  946.    }
  947.    else {
  948.       MM_disableLogging();
  949.    }
  950.  
  951.    // Do cleanup in two passes -- the first pass , the second pass
  952.    // cleans up certain items based on a hierarchy traversal of the DOM.
  953.    //
  954.    MM.setBusyCursor();
  955.    doPassOne();
  956.    doPassTwo();
  957.    MM.clearBusyCursor();
  958.    finalize();
  959. }
  960.  
  961. // initialize()
  962. //
  963. // This is called on BODY onLoad; initialize all script globals
  964. //
  965. function initialize()
  966. {
  967.    // Counters for logging output
  968.    //
  969.    numEmptyRemoved      = 0;
  970.    numRedundantRemoved  = 0;
  971.    numTagsRemoved       = 0;
  972.    numCommentsRemoved   = 0;
  973.    numFontsCombined     = 0;
  974.    bRemovedTracing      = false;
  975.  
  976.    arrTagsToRemove.length = 0; // Empty array
  977.  
  978.    strClassAttrib       = "CLASS";
  979.    strStyleAttrib       = "STYLE";
  980.  
  981.    // The following tags represent the tag names of Dreamweaver-
  982.    // specific comments, which are processed through the Dreamweaver
  983.    // JS API/DOM as named element nodes rather than comment nodes
  984.    //
  985.    arrDWCommentTags.push ( "MM:EDITABLE"
  986.                          , "MM:LIBITEM"       // variable library item (currently unused)
  987.                          , "MM:TEMPLATE"
  988.                          , "MM:UNLOCKATTRS"   // currently unused
  989.                          , "{#CUSTOMOBJ}"
  990.                          , "{#MEINLINE}"      // used by Japanese DW
  991.                          , "{#LIBITEM}" );
  992.  
  993.    // The following tags can be harmlessly removed from the user's
  994.    // document if they're empty.  Note that the Heading tags are
  995.    // not always safe and require special further handling; see
  996.    // isEmptyRemoveableTag().
  997.    //
  998.    emptyRemovalCandidates.push ( "H1", "H2", "H3", "H4", "H5", "H6"
  999.                                , "TT", "I", "B", "U", "STRIKE", "BIG"
  1000.                                , "SMALL", "SUB", "SUP", "EM", "STRONG"
  1001.                                , "DFN", "CODE", "SAMP", "KBD", "VAR"
  1002.                                , "CITE", "XMP", "BLINK"
  1003.                                , "ADDRESS"
  1004.                                , "A"
  1005.                                , "FONT"
  1006.                                , "SPAN"
  1007.                                , "TABLE"
  1008.                                , "BLOCKQUOTE"
  1009.                                , "LI", "OL", "UL"
  1010.                                , "DD", "DT", "DL"
  1011.                                , "DIR", "MENU"
  1012.                                , "DIV", "CENTER" );
  1013.  
  1014.    // These tags can be safely removed if they're redundant
  1015.    // with their immediate parent, i.e., this tags have
  1016.    // no nesting semantics.
  1017.    //
  1018.    redundantTagCandidates.push( "TT", "I", "B", "U", "STRIKE", "BIG"
  1019.                               , "SMALL", "SUB", "SUP", "EM", "STRONG"
  1020.                               , "DFN", "CODE", "SAMP", "KBD", "VAR"
  1021.                               , "CITE", "XMP"
  1022.                               , "FONT"
  1023.                               , "CENTER"
  1024.                               , "SPAN" );
  1025.  
  1026.    // These tags can be safely coalesced with parents with identical
  1027.    // regions of influence.  Currently this is only done for FONT tags.
  1028.    //
  1029.    combinableTagCandidates.push( "FONT" );
  1030.  
  1031.  
  1032.    // Global used by pass two to indicate if the next empty
  1033.    // header we encounter should be preserved -- the first
  1034.    // empty header after text is significant as a carriage
  1035.    // return is forced; after that they can be gobbled until
  1036.    // there's more text.
  1037.    //
  1038.    bPreserveEmptyHeader = false;
  1039.  
  1040.  
  1041.    // And finally reference actual form element names
  1042.    // here once
  1043.    //
  1044.    with( document.optionsForm )
  1045.    {
  1046.       cbRemoveEmptyTags       = removeEmptyTags;
  1047.       cbRemoveRedundant       = removeRedundantChildren;
  1048.       cbRemoveComments        = removeNDWComments;
  1049.       cbRemoveDWComments      = removeDWComments;
  1050.       cbRemoveTags            = removeTag;
  1051.       cbCombineFonts          = combineFonts;
  1052.       cbShowLog               = showLog;
  1053.       tbTagsToRemove          = tagsToRemove;
  1054.    }
  1055. }
  1056.  
  1057.  
  1058.  
  1059. function finalize()
  1060. {
  1061.    // Show what we did if show log is enabled
  1062.    //
  1063.    if ( cbShowLog.checked )
  1064.    {
  1065.       MM_note( MSG_TrcSummaryHeader );
  1066.  
  1067.       var bDidSomething = (numEmptyRemoved > 0)      ||
  1068.                           (numRedundantRemoved > 0)  ||
  1069.                           (numTagsRemoved > 0)       ||
  1070.                           (numCommentsRemoved > 0)   ||
  1071.                           (numFontsCombined > 0)     ||
  1072.                           (bRemovedTracing);
  1073.  
  1074.       if ( bDidSomething )
  1075.       {
  1076.          if ( numEmptyRemoved > 0 )
  1077.             MM_note( MSG_TrcEmptyRemoved, numEmptyRemoved );
  1078.          if ( numRedundantRemoved > 0 )
  1079.             MM_note( MSG_TrcRedundantRemoved, numRedundantRemoved );
  1080.          if ( numTagsRemoved > 0 )
  1081.             MM_note( MSG_TrcTagsRemoved, numTagsRemoved );
  1082.          if ( numCommentsRemoved > 0 )
  1083.             MM_note( MSG_TrcCommentsRemoved, numCommentsRemoved );
  1084.          if ( numFontsCombined > 0 )
  1085.             MM_note( MSG_TrcFontsCombined, numFontsCombined );
  1086.          if ( bRemovedTracing )
  1087.             MM_note( MSG_TracingAttrsRemoved );
  1088.       }
  1089.       else {
  1090.          MM_note( MSG_TrcDidNothing );
  1091.       }
  1092.       MM_showLog();
  1093.    }
  1094.  
  1095.    window.close();
  1096. }
  1097.